Skip to content

fix: describe silently dropped trailing XPath constraint groups (#772) - #80

Merged
ako merged 1 commit into
mainfrom
claude/xpath-constraint-truncation-772
Aug 2, 2026
Merged

fix: describe silently dropped trailing XPath constraint groups (#772)#80
ako merged 1 commit into
mainfrom
claude/xpath-constraint-truncation-772

Conversation

@ako

@ako ako commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Fixes the upstream report mendixlabs/mxcli#772.

Problem

Mendix stores sibling predicate groups concatenated in a single XPathConstraint field:

[Reminders.Task_TaskGroup/Reminders.TaskGroup[EndDate = $EndDateLimit]]
[Status != 'Completed']
[CompletionDate = empty]

describe rendered only the first:

where Reminders.Task_TaskGroup/Reminders.TaskGroup[EndDate = $EndDateLimit];

The reporter's framing is the important part: this is worse than a crash. The output looks complete while describing a materially less restrictive query than the project contains — so correct defensive code reads as buggy, and describe is exactly what an agent reads to decide whether code is right.

Root cause

The grammar's xpathConstraint rule matches one bracket group. ParseXPathConstraint removes the error listeners, so ANTLR parsed the first group, left the rest on the token stream, and still returned ok = true. enrichXPathConstraintForDescribe treated that as a full parse and re-rendered only what came back — its if !ok { return original } fallback never fired.

Confirmed directly before touching anything:

parse ok    = true
re-rendered = "[Reminders.Task_TaskGroup/Reminders.TaskGroup[EndDate = $EndDateLimit]]"

Approach — two layers

  1. ParseXPathConstraint rejects a partial parse (require the token stream at EOF). This alone stops the data loss: the caller falls back to the stored string, which the render path then splits correctly.
  2. visitor.SplitXPathPredicateGroups splits a constraint into top-level groups, each enriched and rendered separately — so enum enrichment reaches groups after the first, not just the first.

The splitter tracks nesting depth and quoting, because the previous "][" split mangles both a nested [A/B[x = 1]] and a literal containing ] ([Name = 'a]b']). Both appear in real projects, and mishandling either silently changes what a query means. The render path now uses the same splitter instead of its own ad-hoc version.

A group that does not parse is passed through verbatim rather than dropped or guessed at.

Verification

End-to-end on a real 11.12.2 project carrying the reported constraint shape:

build describe output
pre-fix where Bug772.Task_TaskGroup/Bug772.TaskGroup[EndDate = $EndDateLimit];
fixed all three groups, with Status != Bug772.TaskStatus.Completed enriched in the second group
  • The describe output re-parses and re-executes to an identical flow (full round-trip).
  • mxcli docker check → 0 errors.
  • Full ./... suite green.

All three guards mutation-checked. Worth noting what that revealed: reverting the per-group split alone does not reproduce the data loss any more, because the strict parse already prevents it — it only degrades enum enrichment ('Completed' instead of the qualified value). That is defence in depth working as intended, and it is why there is a separate enrichXPathGroups unit test rather than relying on the end-to-end one.

Generalisable lesson (recorded in the symptom table)

A parser that silently accepts a prefix is worse than one that fails. Any ok returned by a rule that can match less than its input must be checked against EOF before a caller treats the result as lossless.

Repro script: mdl-examples/bug-tests/772-xpath-constraint-groups.mdl.


Generated by Claude Code

…ixlabs#772)

Mendix stores sibling predicate groups concatenated in one XPathConstraint field:

  [Reminders.Task_TaskGroup/Reminders.TaskGroup[EndDate = $EndDateLimit]]
  [Status != 'Completed']
  [CompletionDate = empty]

The grammar's xpathConstraint rule matches ONE bracket group. ParseXPathConstraint
removes the error listeners, so ANTLR parsed the first group, left the rest on the
token stream, and still returned ok=true. enrichXPathConstraintForDescribe read
that as a full parse and re-rendered only what came back — its
`if !ok { return original }` fallback never fired — so describe emitted:

  where Reminders.Task_TaskGroup/Reminders.TaskGroup[EndDate = $EndDateLimit];

That is worse than a crash. The output looks complete while describing a
materially less restrictive query than the project contains, which makes correct
defensive code read as buggy — and `describe` is what an agent reads to decide
whether code is right.

Fixed in two layers:

1. ParseXPathConstraint reports a partial parse as a failure (require the token
   stream to be at EOF). That alone stops the data loss: the caller falls back to
   the stored string, which the render path then splits correctly.
2. visitor.SplitXPathPredicateGroups splits a constraint into its top-level
   groups, and each is enriched and rendered separately — so enum enrichment
   reaches groups after the first, not just the first. The splitter tracks nesting
   depth and quoting, because the previous "][" split mangled both a nested
   [A/B[x = 1]] and a literal containing ']'. The render path now uses it too.

Verified end-to-end on a real 11.12.2 project carrying the reported constraint
shape: all three groups render, Status is enriched to its qualified enum value in
the second group, the output re-parses and re-executes to an identical flow, and
`mx check` reports 0 errors. A/B against a pre-fix binary on the same project
reproduces the two dropped groups exactly as reported.

All three guards mutation-checked.

Refs mendixlabs#772
@ako
ako merged commit 7b5d29f into main Aug 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants